package org.wheel.plugins.toolkit.jdbc.pool.util;

import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.Statements;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterUtils;
import org.springframework.jdbc.core.namedparam.ParsedSql;
import org.wheel.plugins.toolkit.sql.enums.SqlType;

import javax.sql.DataSource;
import java.sql.*;
import java.util.Locale;
import java.util.Map;

import static org.wheel.plugins.toolkit.sql.enums.SqlType.*;

public class SqlTypeUtil {

    // 直接使用 LogFactory 创建日志对象，Hutool 会自动检测项目中的日志框架（优先级：Slf4j > Log4j > Logback > JDK Logging）
    private static final Log log = LogFactory.get();

    /**
     * 方案一，动态判断，利用jdbc本身，尝试判断SQL类型
     * 如果SQL执行影响的数据比较大，或者操作的表比较大，则不建议使用此方案
     * @param con 数据库连接
     * @param sql SQL语句，如果带有参数，请使用 {@code ParamValueConvert.convertValuesIntoSql(sql, param, dialect);} 方法，将SQL中占位符替换为可执行的参数值
     * @return SQL类型
     */
    public static SqlType guessSqlType(Connection con, String sql) throws SQLException {
        // 1. 保存现场
        boolean oldAutoCommit = con.getAutoCommit();
        boolean oldReadOnly   = con.isReadOnly();
        Savepoint sp = null;
        try {
            // 2. 进入“只读 + 手动提交”模式
            con.setAutoCommit(false);
            con.setReadOnly(true);          // 让数据库拒绝一切写（可选但推荐）

            // 3. 设一个保存点，双重保险
            sp = con.setSavepoint("PROBE_SP");
            try (PreparedStatement ps = con.prepareStatement(sql)) {
                    boolean hasResult = ps.execute();          // 第一个结果
                    if (hasResult) {
                        try (ResultSet rs = ps.getResultSet()) {
                            return SELECT;             // 只要能开出结果集就是查询
                        }
                    } else {
                        long uc = ps.getLargeUpdateCount();  // -1 表示已经没有更多结果
                        if (uc >= 0 || uc == -2) {           // 有更新计数
                            String upper = sql.trim()
                                    .replaceFirst("/\\*.*?\\*/", "")
                                    .replaceFirst("--.*", "")
                                    .toUpperCase(Locale.ROOT);
                            if (upper.startsWith("INSERT")) return INSERT;
                            if (upper.startsWith("UPDATE")) return SqlType.UPDATE;
                            if (upper.startsWith("DELETE")) return DELETE;
                            return SqlType.OTHER;              // MERGE 等
                        }
                    }
                    return SqlType.OTHER;
            }
        } finally {
            // 5. 无论成功失败都回滚到保存点，再恢复现场
            if (sp != null) try { con.rollback(sp); } catch (SQLException ignore) {}
            con.setAutoCommit(oldAutoCommit);
            con.setReadOnly(oldReadOnly);
        }
    }

    /**
     * 方案一 动态判断，利用jdbc本身，尝试判断SQL类型
     * 如果SQL执行影响的数据比较大，或者操作的表比较大，则不建议使用此方案
     * @param ds 数据库连接
     * @param sql SQL语句，":name"占位符
     * @param param 对应的参数
     * @return SQL类型
     * @throws SQLException
     */
    public static SqlType guessSqlType(DataSource ds, String sql, Map<String, Object> param) {
        NamedParameterJdbcTemplate tpl = new NamedParameterJdbcTemplate(ds);

        // 1. 把 Map 包装成 SqlParameterSource
        MapSqlParameterSource source = new MapSqlParameterSource(param);

        // 2. 解析 SQL，得到 ParsedSql 对象
        ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);

        // 3. 转成 jdbc 风格的 ? 语句
        String jdbcSql = NamedParameterUtils.substituteNamedParameters(parsedSql, source);

        // 4. 按顺序拼出 value 数组
        Object[] valueArray = NamedParameterUtils.buildValueArray(parsedSql, source, null);

        // 5. 构造 PreparedStatementCreator
        PreparedStatementCreatorFactory factory =
                new PreparedStatementCreatorFactory(jdbcSql);
        PreparedStatementCreator psc =
                factory.newPreparedStatementCreator(valueArray);

        // 3. 同一条连接里执行
        return tpl.getJdbcTemplate().execute((Connection con) -> {
            // -------- 安全沙箱 --------
            boolean oldAutoCommit = con.getAutoCommit();
            boolean oldReadOnly   = con.isReadOnly();
            Savepoint sp = null;
            try {
                con.setAutoCommit(false);
                con.setReadOnly(true);
                sp = con.setSavepoint("PROBE_SP");

                try (PreparedStatement ps = psc.createPreparedStatement(con)) {
                    // isSelect 只能拿到「SELECT vs WRITE」两分法
                    boolean isSelect = ps.execute();
                    if (isSelect) {
                        return SELECT;
                    }
                    // 走到这里一定是写语句，再细分
                    return extractDmlType(jdbcSql);
                }
            } finally {
                if (sp != null) try { con.rollback(sp); } catch (SQLException ignore) {}
                con.setAutoCommit(oldAutoCommit);
                con.setReadOnly(oldReadOnly);
            }
        });
    }
    private static SqlType extractDmlType(String jdbcSql) {
        // 1. 去掉注释、字符字面量
        String s = jdbcSql
                .replaceAll("/\\*.*?\\*/", " ")
                .replaceAll("--[^\\r\\n]*", " ")
                .replaceAll("'([^']|'')*'", " ")   //-- 单引串
                .replaceAll("\"([^\"]|\"\")*\"", " "); //-- 双引串
        // 2. 取第一个有效 token
        String[] tok = s.split("\\s+");
        for (String t : tok) {
            if (t.isEmpty()) continue;
            switch (t.toUpperCase(Locale.ROOT)) {
                case "INSERT": return INSERT;
                case "UPDATE": return UPDATE;
                case "DELETE": return DELETE;
                default:       return OTHER;   // MERGE / TRUNCATE / DDL …
            }
        }
        return OTHER;
    }

    /**
     * 方案二，静态判断，尝试判断SQL类型
     * 缺点是，因为SQL语句的不规范，可能会导致SQL解析失败，从而无法判断SQL类型
     * 基于 CCJSqlParser 判断SQL类型
     * @param sql SQL语句，可以不用替换占位符
     * @return SQL类型
     */
    public static SqlType guessSqlType(String sql) {
        try {
            Statements stmts = CCJSqlParserUtil.parseStatements(sql);
            if (stmts.getStatements().size() != 1) return OTHER;

            Statement stmt = stmts.getStatements().get(0);
            if (stmt instanceof Select) return SELECT;
            if (stmt instanceof Insert) return INSERT;
            if (stmt instanceof Update) return UPDATE;
            if (stmt instanceof Delete) return DELETE;
            return OTHER;          // Merge, Create, Drop …
        } catch (JSQLParserException e) {
            return OTHER;
        }
    }
}
